/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.auth;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.util.Utils;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import com.google.firebase.internal.NonNull;
import com.google.firebase.tasks.Continuation;
import com.google.firebase.tasks.Task;
import com.google.firebase.tasks.Tasks;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.Callable;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Standard {@link FirebaseCredential} implementations for use with {@link
* com.google.firebase.FirebaseOptions}.
*/
public class FirebaseCredentials {
private static final List<String> FIREBASE_SCOPES =
ImmutableList.of(
"https://www.googleapis.com/auth/firebase.database",
"https://www.googleapis.com/auth/userinfo.email");
private FirebaseCredentials() {
}
private static String streamToString(InputStream inputStream) throws IOException {
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
return CharStreams.toString(reader);
}
/**
* Returns a {@link FirebaseCredential} based on Google Application Default Credentials which can
* be used to authenticate the SDK.
*
* <p>See <a
* href="https://developers.google.com/identity/protocols/application-default-credentials">Google
* Application Default Credentials</a> for details on Google Application Deafult Credentials.
*
* <p>See <a href="/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
* and detailed documentation.
*
* @return A {@link FirebaseCredential} based on Google Application Default Credentials which can
* be used to authenticate the SDK.
*/
@NonNull
public static FirebaseCredential applicationDefault() {
return DefaultCredentialsHolder.INSTANCE;
}
@VisibleForTesting
static FirebaseCredential applicationDefault(HttpTransport transport, JsonFactory jsonFactory) {
return new ApplicationDefaultCredential(transport, jsonFactory);
}
/**
* Returns a {@link FirebaseCredential} generated from the provided service account certificate
* which can be used to authenticate the SDK.
*
* <p>See <a href="/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
* and detailed documentation.
*
* @param serviceAccount An <code>InputStream</code> containing the JSON representation of a
* service account certificate.
* @return A {@link FirebaseCredential} generated from the provided service account certificate
* which can be used to authenticate the SDK.
* @throws IOException If an error occurs while parsing the service account certificate.
*/
@NonNull
public static FirebaseCredential fromCertificate(InputStream serviceAccount) throws IOException {
return fromCertificate(serviceAccount, Utils.getDefaultTransport(),
Utils.getDefaultJsonFactory());
}
@VisibleForTesting
static FirebaseCredential fromCertificate(InputStream serviceAccount, HttpTransport transport,
JsonFactory jsonFactory) throws IOException {
return new CertCredential(serviceAccount, transport, jsonFactory);
}
/**
* Returns a {@link FirebaseCredential} generated from the provided refresh token which can be
* used to authenticate the SDK.
*
* <p>See <a href="/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
* and detailed documentation.
*
* @param refreshToken An <code>InputStream</code> containing the JSON representation of a refresh
* token.
* @return A {@link FirebaseCredential} generated from the provided service account credential
* which can be used to authenticate the SDK.
* @throws IOException If an error occurs while parsing the refresh token.
*/
@NonNull
public static FirebaseCredential fromRefreshToken(InputStream refreshToken) throws IOException {
return fromRefreshToken(
refreshToken, Utils.getDefaultTransport(), Utils.getDefaultJsonFactory());
}
@VisibleForTesting
static FirebaseCredential fromRefreshToken(final InputStream refreshToken,
HttpTransport transport, JsonFactory jsonFactory) throws IOException {
return new RefreshTokenCredential(refreshToken, transport, jsonFactory);
}
/**
* Helper class that implements {@link FirebaseCredential} on top of {@link GoogleCredential} and
* provides caching of access tokens and credentials.
*/
abstract static class BaseCredential implements FirebaseCredential {
final HttpTransport transport;
final JsonFactory jsonFactory;
private GoogleCredential googleCredential;
BaseCredential(HttpTransport transport, JsonFactory jsonFactory) {
this.transport = checkNotNull(transport, "HttpTransport must not be null");
this.jsonFactory = checkNotNull(jsonFactory, "JsonFactory must not be null");
}
/** Retrieves a GoogleCredential. Should not use caching. */
abstract GoogleCredential fetchCredential() throws IOException;
/**
* Returns the associated GoogleCredential for this class. This implementation is cached by
* default.
*/
final Task<GoogleCredential> getCertificate() {
synchronized (this) {
if (googleCredential != null) {
return Tasks.forResult(googleCredential);
}
}
return Tasks.call(
new Callable<GoogleCredential>() {
@Override
public GoogleCredential call() throws Exception {
// Retrieve a new credential. This is a network operation that can be repeated and is
// done outside of the lock.
GoogleCredential credential = fetchCredential();
synchronized (BaseCredential.this) {
googleCredential = credential;
}
return credential;
}
});
}
abstract GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException;
/**
* Returns an access token for this credential. Does not cache tokens.
*/
@Override
public final Task<GoogleOAuthAccessToken> getAccessToken() {
return getCertificate()
.continueWith(new Continuation<GoogleCredential, GoogleOAuthAccessToken>() {
@Override
public GoogleOAuthAccessToken then(@NonNull Task<GoogleCredential> task)
throws Exception {
return fetchToken(task.getResult());
}
});
}
}
static class CertCredential extends BaseCredential {
private final String jsonData;
private final String projectId;
CertCredential(InputStream inputStream, HttpTransport transport,
JsonFactory jsonFactory) throws IOException {
super(transport, jsonFactory);
jsonData = streamToString(checkNotNull(inputStream));
JSONObject jsonObject = new JSONObject(jsonData);
try {
projectId = jsonObject.getString("project_id");
} catch (JSONException e) {
throw new IOException("Failed to parse service account: 'project_id' must be set", e);
}
}
@Override
GoogleCredential fetchCredential() throws IOException {
GoogleCredential firebaseCredential =
GoogleCredential.fromStream(
new ByteArrayInputStream(jsonData.getBytes("UTF-8")), transport, jsonFactory);
if (firebaseCredential.getServiceAccountId() == null) {
throw new IOException(
"Error reading credentials from stream, 'type' value 'service_account' not "
+ "recognized. Expecting 'authorized_user'.");
}
return firebaseCredential.createScoped(FIREBASE_SCOPES);
}
@Override
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
credential.refreshToken();
return newAccessToken(credential);
}
Task<String> getProjectId() {
return Tasks.forResult(projectId);
}
}
static class ApplicationDefaultCredential extends BaseCredential {
ApplicationDefaultCredential(HttpTransport transport, JsonFactory jsonFactory) {
super(transport, jsonFactory);
}
@Override
GoogleCredential fetchCredential() throws IOException {
return GoogleCredential.getApplicationDefault(transport, jsonFactory)
.createScoped(FIREBASE_SCOPES);
}
@Override
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
credential.refreshToken();
return newAccessToken(credential);
}
}
static class RefreshTokenCredential extends BaseCredential {
private final String jsonData;
RefreshTokenCredential(InputStream inputStream, HttpTransport transport,
JsonFactory jsonFactory) throws IOException {
super(transport, jsonFactory);
jsonData = streamToString(checkNotNull(inputStream));
}
@Override
GoogleCredential fetchCredential() throws IOException {
GoogleCredential credential =
GoogleCredential.fromStream(
new ByteArrayInputStream(jsonData.getBytes("UTF-8")), transport, jsonFactory);
if (credential.getServiceAccountId() != null) {
throw new IOException(
"Error reading credentials from stream, 'type' value 'authorized_user' not "
+ "recognized. Expecting 'service_account'.");
}
return credential;
}
@Override
GoogleOAuthAccessToken fetchToken(GoogleCredential credential) throws IOException {
credential.refreshToken();
return newAccessToken(credential);
}
}
private static class DefaultCredentialsHolder {
static final FirebaseCredential INSTANCE =
applicationDefault(Utils.getDefaultTransport(), Utils.getDefaultJsonFactory());
}
static GoogleOAuthAccessToken newAccessToken(GoogleCredential credential) {
checkNotNull(credential);
return new GoogleOAuthAccessToken(credential.getAccessToken(),
credential.getExpirationTimeMilliseconds());
}
}